#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cerrno>
#include<unordered_map>
#include<unordered_set>
#include<functional>

#define ARRAY_MAX 1024

//上一个进程的退出码
int lastcode = 0;

//1.环境变量表(全局)
#define ENV_NUM 100
char* g_env[ENV_NUM];
int EnvNum = 0;//当前进程下，环境变量个数
extern char** environ;

//2.命令行参数表
#define ARGV_NUM 100
char* g_argv[ARGV_NUM];
int g_argc = 0;

//3.别名表
std::unordered_map<std::string, std::string> Nickeys;

//4.内建命令表

/*cd命令*/
bool CD(){
  char* To_path = g_argv[1];
  if(To_path == NULL){
    To_path = getenv("HOME");
    chdir(To_path);
    lastcode = 0;
    return true;
  }
  int flag = chdir(To_path);
  if(flag == -1){
    std::cout << "-mybash cd: " << To_path << ": " << strerror(errno) << std::endl;
  }
  lastcode = flag;
  return flag;
}

/*export命令*/
//这个命令比较复杂 就实现一个简单版本的就好了

bool EXPORT(){
  if(g_argc == 1){
    //只使用export的时候，就单纯打印一下当前环境变量表好了
    for(int i = 0; environ[i]; ++i){
      std::cout << "declare -x " << environ[i] << std::endl;
    } 
    lastcode = 0;
    return true;
  }
  std::string env(g_argv[1]);
  size_t EqualPos = env.find('=');
  if(EqualPos == std::string::npos){
    std::cout << "Environment Arguments Set Error" << std::endl;
    lastcode = 1;
    return false;
  }
  std::string key(env.begin(), env.begin() + EqualPos);
  ++EqualPos;
  std::string value(env.begin() + EqualPos, env.end());

  char tmp[256];
  snprintf(tmp, 256, "%s=%s", key.c_str(), value.c_str());
  char* buffer = (char*)malloc(strlen(tmp) + 1);
  strncpy(buffer, tmp, strlen(tmp) + 1);

  int flag = putenv(buffer);
  g_env[EnvNum++] = buffer;
  g_env[EnvNum] = nullptr;
  lastcode = (flag == 0);
  return flag == 0;
}

bool ECHO(){
  //echo $? -> 上一次进程的退出码
  //echo $环境变量
  //...echo还有很多功能 这里就不写了 稍微模拟一下就好了(所以写法比较简单)
  //echo 打印字符串
  
  char* PRINT = g_argv[1];
  if(PRINT == nullptr){
    std::cout << std::endl;
    lastcode = 0;
    return true;
  }
  if(PRINT[0] == '$'){
    if(PRINT[1] == '?'){
      std::cout << lastcode << std::endl;
      lastcode = 0;
      return true;
    }
    else{
      std::string print(PRINT);
      std::string env_argu = print.substr(1); 
      char* ret = getenv(env_argu.c_str());
      if(ret == NULL){
        std::cout << std::endl; 
      }
      else std::cout << ret << std::endl;
      lastcode = 0;
      return true;
    }
  }
  else{
    std::cout << PRINT << std::endl;
    lastcode = 0;
    return true;
  }
  return true;
}

//如果是用c++写就很方便，使用哈希map，第一个是指令，第二个是对应的操作(包装器)
//这个内建命令表是全局的
std::unordered_map<std::string, std::function<bool()>> BulitInCmd_match = 
{
  {"cd", CD}, {"export", EXPORT}, {"echo", ECHO}
};
//这里就不搞那么多内建命令和份文件规范了，直接在下面写点内建命令就好了


/*一些相关的函数*/

/*初始化环境变量表，并导入到当前进程*/
void Init_Env(){
  for(EnvNum = 0; environ[EnvNum]; ++EnvNum){
    g_env[EnvNum] = environ[EnvNum];
    putenv(g_env[EnvNum]);
  }
  char* DIFF = (char*)"DIFF=lululu";//用来识别是自己写的shell中的环境变量表
  g_env[EnvNum] = (char*)calloc(strlen(DIFF) + 1, 1);
  for(size_t i = 0; i < strlen(DIFF); ++i){
    g_env[EnvNum][i] = DIFF[i];
  }

  putenv(g_env[EnvNum]);
  g_env[++EnvNum] = nullptr;
}

/*初始化一个别名表*/
/*这里要注意的是，因为文件内使用fscanf读取的时候会读不到指令的空格 所以指令如果带有选项得使用其他符号(这里用的^)连接了，然后处理在变成空格*/
void Init_Alias(){
  FILE* fp = fopen("./CMD_NickName.txt", "r");
  if(fp == NULL){
    perror("fopen");
    exit(1);
  }
  char Nick[ARRAY_MAX]; 
  char COMMAND[ARRAY_MAX];
  while(!feof(fp)){
    fscanf(fp, "%s %s\n", Nick, COMMAND);
    std::string nick(Nick);
    std::string command(COMMAND);
    //对于command这个string，需要处理一下所有的^
    for(auto& it : command){
      if(it == '^') it = ' ';
    } 
    Nickeys[nick] = command;
    memset(Nick, 0, sizeof(Nick));
    memset(COMMAND, 0, sizeof(COMMAND));
  } 
  fclose(fp);
}

/*常用环境变量的获取*/
char* GetUser(){
  char* User = getenv("USER");
  return (User == nullptr) ? (char*)" " : User;
}

char* GetHostName(){
  char* HostName = getenv("HOSTNAME");
  return (HostName == nullptr) ? (char*)" " : HostName;
}

#define CWDNUM 1024
char CWD[CWDNUM];
char CWDENV[CWDNUM];
char* GetPwd(){
  //需要从当前的环境变量表里面去查找Pwd
  
  char* cwd = getcwd(CWD, sizeof(CWD));
  if(cwd == nullptr) std::cout << "CWD ERROR" << std::endl;
  //现在可能会有一个问题，就是使用了cd命令后，路径变了，但是环境变量表中的PWD没变
  snprintf(CWDENV, sizeof(CWDENV), "PWD=%s", CWD);
  putenv(CWDENV);
  return CWD;
}


/*打印命令行基本信息 ：[ynp@hcss-ecs-1643 MyShell]*/
void Print_CmdInfo(){
  char buffer[ARRAY_MAX];
  std::string lastdir(GetPwd());
  size_t pos = lastdir.rfind('/', lastdir.size() - 1);
  ++pos;
  snprintf(buffer, sizeof(buffer), "[%s@%s %s]%% ", GetUser(), GetHostName(), lastdir.c_str() + pos);
  std::cout << buffer;
}

/*接收用户的输入*/
bool GetUserInput(char* input, int size){
  char* ret = fgets(input, size, stdin);
  if(ret == nullptr){
    std::cout << "命令读取错误" << " 错误为" << strerror(errno) << std::endl; 
    return false;
  }
  int length = strlen(input);//length必然是>0的，因为至少要输入一个'\n'
  //此时length - 1的位置是'\n'
  input[length - 1] = 0;
  if(strlen(input) == 0){
    return false;
  } 
  return true;
}

/*分析命令行参数*/
void CmdAnalyse(char* cmd){
  g_argc = 0;
  memset(g_argv, 0, sizeof(g_argv));
  const char* sep = " ";
  g_argv[g_argc++] = strtok(cmd, sep);
  while((g_argv[g_argc++] = strtok(NULL, sep)));
  --g_argc;
}

/*检查是否出现在别名表 如果没有就直接返回 如果出现了就需要重新分析命令行参数*/
void CheckAlias(const char* cmd, char* userinput, int userinput_Size){
  auto it = Nickeys.find(std::string(cmd));
  //在别名表里面
  if(it != Nickeys.end()){
    std::string realCMD = Nickeys[std::string(cmd)];
    memset(userinput, 0, userinput_Size);
    for(size_t i = 0; i < realCMD.size(); ++i){
      userinput[i] = realCMD[i];
    }
    CmdAnalyse(userinput);
    return;
  }
}

/*检查是否为内建命令*/
//如果是内建命令，就需要让父进程自己执行，但是如果不是，那么就不需要执行
bool CheckBuiltInCmd(std::string cmd){
  const char* BulitInCmd = cmd.c_str();
  if(BulitInCmd_match.find(std::string(BulitInCmd)) != BulitInCmd_match.end()){
    //内建命令表搜寻成功 -> 执行命令
    bool flag = BulitInCmd_match[std::string(BulitInCmd)]();
    return flag;
  }
  return false;
}


/*创建子进程执行命令*/
bool ExecuteCmd(std::string cmd){
 pid_t id = fork();
 if(id < 0){
   perror("fork");
   lastcode = errno;
   return false;
 }
 else if(id == 0){
  //子进程 -> 程序替换(子进程被替换)，因为父进程是shell本身，不能变
  //使用进程替换的函数
  execvpe(cmd.c_str(), g_argv, g_env);
  std::cout << "-mybash: " << cmd.c_str() << ": " << "command not found" << std::endl;
  exit(23);
 }
 else{
   //父进程
   int status = 0;
   int rid = waitpid(id, &status, 0);
   lastcode = WEXITSTATUS(status);
   if(rid < 0){
     std::cout << "进程执行错误：" << "接收信号为" << WTERMSIG(status) << std::endl;
     if(WTERMSIG(status) == 0)
       std::cout << "程序正常执行，退出码为" << WEXITSTATUS(status);
     else std::cout << "程序异常退出" << std::endl;
    return false;
   }
   return true;
 }
}


/*测试代码*/

/*测试环境变量表是否打印成功*/
void InitENV_Test(){
  for(int i = 0; environ[i]; ++i){
    std::cout << "Env[" << i << "]" << " -> " << environ[i] << std::endl;
  } 
}

/*测试打印argv表*/
void Print_Argv(){
  std::cout << "echo ";
  for(int i = 0; g_argv[i]; ++i){
    std::cout << g_argv[i] << " ";
  } 
  std::cout << std::endl;
}




int main(){
  //导入环境变量表 但是要说明的是：
  //真正的shell程序是从配置文件中导入的环境变量，但是那是shell脚本语言。
  //所以这里就模拟以下意思一下就好了
  Init_Env();
  Init_Alias();//别名表的初始化
  char UserInput[ARRAY_MAX];

  //然后就是不停歇的执行shell程序
  while(1){
    //1.先打印和bash进程中的命令行行最前端的一些基本信息
    Print_CmdInfo();
  
    //2.然后进程会一直阻塞在输入界面，需要等到用户输入
    if(GetUserInput(UserInput, ARRAY_MAX) == false) continue;

    //3.分析命令函参数，其实就是切分到argv表里面
    CmdAnalyse(UserInput); 
    std::string CMD = g_argv[0];//指令的名称

    //4.可能会出现有别名比如ll -> ls -l //先检查是否有别名出现，如果有的话，需要把此时别名指令替换为真正的指令
    CheckAlias(CMD.c_str(), UserInput, sizeof(UserInput));
    
    //由于别名的存在，所以此时真正的命令应该是被重新导入到g_argv表里面了
    //需要重新获取命令的名字
    CMD = g_argv[0];

    //5.内建命令的处理 有些命令是不能让子进程执行的
    if(CheckBuiltInCmd(CMD)) continue;
    
    
    //6.命令处理 创建子进程处理
    ExecuteCmd(CMD);
  } 

  return 0;
}
